#!/usr/bin/python3

# Copyright (c) 2020-2021 Fpemud <fpemud@sina.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


import os
import pathlib
from .util import Util, MachineIdFile
from .const import MachineType, ComponentType, DeviceType, ConnectorType, OfflineReason
from .handy import Handy, MachineInfoFile, MachineHwcfgFile, SeatUdevRule


class Vendor:

    def __init__(self, vendorImpl):
        self._i = vendorImpl

    @property
    def id(self):
        return self._i.get_id()

    @property
    def name(self):
        return self._i.get_name()

    def get_all_products(self):
        return self._i.get_all_products()


class Machine:

    def __init__(self, vendor, model, serial, machineType, arch):
        # ready state
        self._ready = False

        # read: self.get_vendor(), self.get_model()
        self._vendor = vendor
        self._model = model
        assert self._vendor is not None and self._model is not None

        # read:      self.get_serial()
        # pre-ready: self._setSerial()
        self._serial = serial

        # read: self.get_type()
        self._machineType = machineType
        assert isinstance(self._machineType, MachineType)

        # read: self.get_arch()
        self._arch = arch

        # read: self.get_id()
        self._id = MachineIdFile.read()    # self._id can be None

        # read:      self.get_components(), self.get_devices(), self.get_connectors()
        # pre-ready: self._addComponent(), self._addDevice(), self._addConnector()
        self._components = []
        self._devices = []
        self._connectors = []

        # read:      self.get_seats()
        # write:     self.add_seat(), self.remove_seat()
        # pre-ready: self._addSeat()
        self._seats = []

        # read:  self.get_name()
        # write: self.set_name()
        self._name = MachineInfoFile.getMachineName()

    def get_vendor(self):
        # returns vendor id
        return self._vendor

    def get_model(self):
        return self._model

    def get_serial(self):
        return self._serial

    def get_type(self):
        return self._machineType

    def get_arch(self):
        return self._arch

    def get_id(self):
        return self._id

    def get_components(self):
        return self._components

    def get_devices(self):
        return self._devices

    def get_connectors(self):
        return self._connectors

    def get_seats(self):
        return self._seats

    def get_name(self):
        # make Machine, Component, Device, Connector have same behavior
        if self._ready:
            return self._name
        else:
            return None

    def add_seat(self, seat_name):
        assert self._ready

        # pre check
        assert seat_name not in self._seat

        # do work
        # FIXME
        assert False

    def remove_seat(self, seat_name):
        assert self._ready

        # pre check
        assert seat_name in self._seat

        # do work
        # FIXME
        assert False

    def set_name(self, name):
        assert self._ready

        # pre check
        if name is not None:
            assert MachineInfoFile.checkMachineName(name)

        # do work
        if name == self._name:
            return
        self._name = name

        # post check
        assert not Handy.hasDuplicateNames(self)

        # write config file
        MachineInfoFile.saveMachineName(self._name)

    def check(self, auto_fix=False, error_callback=None):
        assert self._ready

        errCb = error_callback if error_callback is not None else Util.doNothing

        if self._id is None:
            errCb("No machine id configured.")

        if self._name is None:
            errCb("No machine name configured.")

        machineInfoDict = MachineInfoFile.getMachineInfo()
        if "HARDWARE_VENDOR" in machineInfoDict:
            if machineInfoDict["HARDWARE_VENDOR"] != self._vendor:
                errCb("invalid HARDWARE_VENDOR property in %s." % (MachineInfoFile.PATH))
        if "HARDWARE_MODEL" in machineInfoDict:
            if machineInfoDict["HARDWARE_MODEL"] != self._model:
                errCb("invalid HARDWARE_MODEL property in %s." % (MachineInfoFile.PATH))
        if "CHASSIS" in machineInfoDict:
            if machineInfoDict["CHASSIS"] != self._machineType.value:
                errCb("invalid CHASSIS property in %s." % (MachineInfoFile.PATH))

        # FIXME
        # fpemud-os: kill CONFIG_VT, use seat-main instead of seat0
        # should be removed after logind can use seat0
        if True:
            fullfn, buf = SeatUdevRule.generate("seat-main", [], [])
            if not os.path.exists(fullfn) or pathlib.Path(fullfn).read_text() != buf:
                if auto_fix:
                    with open(fullfn, "w") as f:
                        f.write(buf)
                else:
                    errCb("Invalid content for %s." % (fullfn))

    def _setSerial(self, serial):
        assert not self._readyCheck()
        assert self._vendor is not None
        assert self._serial is None

        self._serial = serial

    def _addComponent(self, component):
        assert not self._ready
        assert isinstance(component, Component) and component._parent is None
        assert component.get_seat() is None or component.get_seat() in self._seats

        self._components.append(component)
        component._parent = self

    def _addDevice(self, device):
        assert not self._ready
        assert isinstance(device, Device) and device._parent is None

        self._devices.append(device)
        device._parent = self

    def _addConnector(self, connector):
        assert not self._ready
        assert isinstance(connector, Connector) and connector._parent is None

        self._connectors.append(connector)
        connector._parent = self

    def _makeReady(self):
        assert not self._ready

        # check
        assert not MachineUtil.hasDuplicateIds(self)

        # add properties and check
        MachineHwcfgFile.catProperties().add_properties_for_machine_components_devices_connectors(self)
        assert not MachineUtil.hasDuplicateNames(self)

        # calculate connections
        pass

        self._ready = True


class Component:

    def __init__(self, vendor, model, serial, componentType, id, offlineReason, saveInfo):
        # parent machine
        self._parent = None

        # read: self.get_vendor(), self.get_model()
        self._vendor = vendor
        self._model = model
        assert self._vendor is not None and self._model is not None

        # read:      self.get_serial()
        # pre-ready: self._setSerial()
        self._serial = serial

        # read: self.get_type()
        self._componentType = componentType
        assert isinstance(self._componentType, ComponentType)

        # read: self.get_id()
        self._id = id
        assert self._id is not None

        # read:      self.is_online(), self.get_offline_reason()
        # pre-ready: self._setOfflineReason()
        self._offlineReason = offlineReason
        if self._offlineReason is not None:
            assert isinstance(self._offlineReason, OfflineReason)

        # read:      self.get_devices(), self.get_connectors()
        # pre-ready: self._addDevice(), self._addConnector()
        self._devices = []
        self._connectors = []

        # read:  self.get_seat()
        # write: self.set_seat()
        self._seat = None

        # read:      self.get_name()
        # write:     self.set_name()
        # pre-ready: self._setName()
        self._name = None

        # internal: self._getSaveInfo()
        self._saveInfo = saveInfo

    @property
    def parent(self):
        assert self._parent is not None
        return self._parent

    def get_vendor(self):
        # returns vendor id
        return self._vendor

    def get_model(self):
        return self._model

    def get_serial(self):
        return self._serial

    def get_type(self):
        return self._componentType

    def get_id(self):
        return self._id

    def is_online(self):
        return (self._offlineReason is None)

    def get_offline_reason(self):
        return self._offlineReason

    def get_devices(self):
        return self._devices

    def get_connectors(self):
        return self._connectors

    def get_seat(self):
        return self._seat

    def get_name(self):
        return self._name

    def set_seat(self, seat_name):
        assert self._readyCheck()

        # FIXME
        # self._seat = seat_name

    def set_name(self, name):
        assert self._readyCheck()

        # do set
        if name == self._name:
            return
        self._name = name

        # post-set check
        assert not Handy.hasDuplicateNames(self)

        # save to config file
        pass

    def _setSerial(self, serial):
        assert not self._readyCheck()
        assert self._vendor is not None
        assert self._serial is None

        self._serial = serial

    def _addDevice(self, device):
        assert not self._readyCheck()
        assert isinstance(device, Device) and device._parent is None

        self._devices.append(device)
        device._parent = self

    def _addConnector(self, connector):
        assert not self._readyCheck()
        assert isinstance(connector, Connector) and connector._parent is None

        self._connectors.append(connector)
        connector._parent = self

    def _setOfflineReason(self, offlineReason):
        assert self._offlineReason == OfflineReason.UNKNOWN
        assert isinstance(offlineReason, OfflineReason)
        assert offlineReason != OfflineReason.UNKNOWN

        self._offlineReason = offlineReason

    def _setName(self, name):
        assert not self._readyCheck()
        assert self._name is None

        self._name = name

    def _getSaveInfo(self):
        return self._saveInfo

    def _getMachine(self):
        if self._parent is None:
            return None
        elif isinstance(self._parent, Machine):
            return self._parent
        else:
            assert False

    def _readyCheck(self):
        m = self._getMachine()
        return m is not None and m._ready


class Device:

    def __init__(self, vendor, model, deviceType, id, offlineReason):
        # parent machine or parent component
        self._parent = None

        # read: self.get_vendor(), self.get_model()
        self._vendor = vendor
        self._model = model
        if self._vendor is not None:
            assert self._model is not None
        else:
            assert self._model is None

        # read: self.get_type()
        self._deviceType = deviceType
        assert isinstance(self._deviceType, DeviceType)

        # read: self.get_id()
        self._id = id
        assert self._id is not None

        # read:      self.is_online(), self.get_offline_reason()
        # pre-ready: self._setOfflineReason()
        self._offlineReason = offlineReason
        if self._offlineReason is not None:
            assert isinstance(self._offlineReason, OfflineReason)
            assert self._offlineReason not in [OfflineReason.REMOVED]

        # read:      self.get_name()
        #            Can do the above operation only when machine is ready
        # write:     self.set_name()
        # pre-ready: self._setName()
        self._name = None

    @property
    def parent(self):
        assert self._parent is not None
        return self._parent

    def get_vendor(self):
        # returns vendor id
        return self._vendor

    def get_model(self):
        return self._model

    def get_type(self):
        return self._deviceType

    def get_id(self):
        return self._id

    def is_online(self):
        return (self._offlineReason is None)

    def get_offline_reason(self):
        return self._offlineReason

    def get_name(self):
        return self._name

    def set_name(self, name):
        assert self._readyCheck()

        # do set
        if name == self._name:
            return
        self._name = name

        # post set check
        assert not Handy.hasDuplicateNames(self)

        # FIXME: record to file
        pass

    def _setOfflineReason(self, offlineReason):
        assert self._offlineReason == OfflineReason.UNKNOWN
        assert isinstance(offlineReason, OfflineReason)
        assert offlineReason not in [OfflineReason.UNKNOWN, OfflineReason.REMOVED]

        self._offlineReason = offlineReason

    def _setName(self, name):
        assert not self._readyCheck()

        self._name = name

    def _getMachine(self):
        if self._parent is None:
            return None
        if isinstance(self._parent, Machine):
            return self._parent
        elif isinstance(self._parent, Component):
            if self._parent._parent is None:
                return None
            else:
                return self._parent._parent
        else:
            assert False

    def _readyCheck(self):
        m = self._getMachine()
        return m is not None and m._ready


class Connector:

    def __init__(self, connectorType, id, offlineReason):
        # parent machine or parent component
        self._parent = None

        # read: self.get_type(), self.is_male_connector(), self.is_female_connector()
        self._connectorType = connectorType
        assert isinstance(self._connectorType, ConnectorType)

        # read: self.get_id()
        self._id = id
        assert self._id is not None

        # read:      self.is_online(), self.is_disabled(), self.get_offline_reason()
        # pre-ready: self._setOfflineReason()
        self._offlineReason = offlineReason
        if self._offlineReason is not None:
            assert isinstance(self._offlineReason, OfflineReason)
            assert self._offlineReason not in [OfflineReason.REMOVED]

        # read:      self.get_name()
        # write:     self.set_name()
        # pre-ready: self._setName()
        self._name = None

        # read:  self.get_peer_connector()
        #        Can do the above operation only when machine is ready
        # write: self._setPeerConnector()
        self._peerConnector = None

    @property
    def parent(self):
        assert self._parent is not None
        return self._parent

    def get_type(self):
        return self._connectorType

    def is_male_connector(self):
        return self._connectorType.is_male_connector()

    def is_female_connector(self):
        return self._connectorType.is_female_connector()

    def get_id(self):
        return self._id

    def is_online(self):
        return (self._offlineReason is None)

    def get_offline_reason(self):
        return self._offlineReason

    def get_name(self):
        return self._name

    def get_peer_connector(self):
        assert self._readyCheck()

        return self._peerConnector

    def set_name(self, name):
        assert self._readyCheck()

        # do set
        if name == self._name:
            return
        self._name = name

        # post set check
        assert not Handy.hasDuplicateNames(self)

        # FIXME: record to file
        pass

    def _setOfflineReason(self, offlineReason):
        assert self._offlineReason == OfflineReason.UNKNOWN
        assert isinstance(offlineReason, OfflineReason)
        assert offlineReason not in [OfflineReason.UNKNOWN, OfflineReason.REMOVED]

        self._offlineReason = offlineReason

    def _setName(self, name):
        assert not self._readyCheck()

        self._name = name

    def _setPeerConnector(self, connector):
        assert not self._readyCheck()

        self._peerConnector = connector

    def _getMachine(self):
        if self._parent is None:
            return None
        if isinstance(self._parent, Machine):
            return self._parent
        elif isinstance(self._parent, Component):
            if self._parent._parent is None:
                return None
            else:
                return self._parent._parent
        else:
            assert False

    def _readyCheck(self):
        m = self._getMachine()
        return m is not None and m._ready


class MachineUtil:

    @classmethod
    def hasDuplicateIds(cls, obj):
        return False
        # return cls._hasDuplicateX(obj,
        #                           lambda m: (None, None),
        #                           lambda c: (c.get_type(), c.get_id()),
        #                           lambda d: (d._parent, d.get_type(), d.get_id()),
        #                           lambda x: x[1] is not None)

    @classmethod
    def hasDuplicateNames(cls, obj):
        return False
        # return cls._hasDuplicateX(obj,
        #                           lambda m: m.get_name(),
        #                           lambda c: c.get_name(),
        #                           lambda d: d.get_name(),
        #                           lambda x: x is not None)

    @staticmethod
    def _hasDuplicateX(obj, machineFunc, componentFunc, deviceFunc, validTestFunc):

        def _getForMachine(obj):
            tlist = [machineFunc(obj)]
            for c in obj._components:
                tlist.append(componentFunc(c))
                for d in c._getDevices():
                    tlist.append(deviceFunc(d))
            for d in obj._devices:
                tlist.append(deviceFunc(d))
            return tlist

        def _getForComponent(obj):
            tlist = []
            tlist.append(componentFunc(obj))
            for d in obj._devices:
                tlist.append(deviceFunc(obj))
            return tlist

        if isinstance(obj, Machine):
            tlist = _getForMachine(obj)
        elif isinstance(obj, Component):
            if obj._parent is not None:
                tlist = _getForMachine(obj._parent)
            else:
                tlist = _getForComponent(obj)
        elif isinstance(obj, Device):
            if obj._parent is not None:
                if obj._parent._parent is not None:
                    tlist = _getForMachine(obj._parent._parent)
                else:
                    tlist = _getForComponent(obj._parent)
            else:
                return False
        else:
            assert False

        tlist = [x for x in tlist if validTestFunc(x)]
        return len(set(tlist)) == len(tlist)
